Skip to content

Snowbridge: Linear-parameterized receipt-log benchmarks#12177

Open
yrong wants to merge 4 commits into
paritytech:masterfrom
yrong:improve-receipt-log-benchmark
Open

Snowbridge: Linear-parameterized receipt-log benchmarks#12177
yrong wants to merge 4 commits into
paritytech:masterfrom
yrong:improve-receipt-log-benchmark

Conversation

@yrong
Copy link
Copy Markdown
Contributor

@yrong yrong commented May 25, 2026

Summary

Replaces the constant-time submit / submit_delivery_receipt weights for snowbridge's three Ethereum-event-consuming extrinsics (pallet-inbound-queue::submit,
pallet-inbound-queue-v2::submit, pallet-outbound-queue-v2::submit_delivery_receipt) with Linear<MIN, MAX> benchmarks parameterized by the actual cost drivers: receipt-proof
node count n and receipt size s. Charges callers in proportion to the work the verifier does on their input rather than worst-case.

Why

Today these extrinsics declare a single constant weight, which has two failure modes:

  • Overcharge for small messages. A 1-node, 320-byte minimal receipt pays the same weight as a 32-node, 8 KiB receipt.
  • Undercharge for large ones if the constant was tuned for a small fixture, leaking block weight to the relayer.

Both costs are dominated by:

  • RLP decode + trie-branch traversal — linear in n (proof-node count).
  • Receipt body decode + log scan — linear in s (receipt envelope length).

So a 2-D linear fit over (n, s) is the right shape.

How

Introduces a shared dynamic-fixture builder in snowbridge-pallet-ethereum-client-fixtures::dynamic (the inverse of pallet-ethereum-client::Pallet::verify) that synthesizes EventFixtures mirroring the verifier's SSZ + merkle + receipts-trie logic.

@yrong yrong marked this pull request as ready for review May 26, 2026 08:06
@paritytech-review-bot paritytech-review-bot Bot requested a review from a team May 26, 2026 08:07
}
}

/// Build a real receipts trie with `n` leaves, each holding the same `s`-byte receipt
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

n is documented as nodes everywhere but here it is documented as leaves. Are we just using leaves to build up the the receipt proof with mock data (no nodes).

// `MaxMessageSize` (the upper bound on the inbound message payload).
let max_message_size = T::MaxMessageSize::get();
let weight_fee = T::WeightToFee::weight_to_fee(&T::WeightInfo::submit(
max_message_size,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are parsing in max_message_size as the proof node count. Wont this bloat the delivery cost?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, seems like it could result in a 25x increase in cost. We need to check if this won't result in unexpected problems.

Comment thread prdoc/pr_12177.prdoc
- audience: Runtime Dev
description: |-
Replaces the constant-time weights for snowbridge's three Ethereum-event-consuming extrinsics with `Linear<MIN, MAX>` benchmarks parameterized by the actual cost drivers: receipt-proof node count `n` and receipt envelope size `s`. Affects `pallet-inbound-queue::submit`, `pallet-inbound-queue-v2::submit`, and `pallet-outbound-queue-v2::submit_delivery_receipt`.
Each pallet's `BenchmarkHelper::initialize_storage` now takes `(n, s)`, the corresponding `WeightInfo` method takes `(u32, u32)`, and the dispatch reads `n` / `s` from `event.proof.receipt_proof` so callers are charged in proportion to verifier work rather than worst-case. Adds runtime-benchmarks-only Config items `MaxProofNodes` and `MaxReceiptBytes` as benchmark-fit upper bounds (they do NOT bound proof or receipt sizes at runtime; the verifier already enforces gas-style cost limits).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the verifier already enforces gas-style cost limits

What do we mean by this?

/// proof. Does NOT bound proof size at runtime — the inbound message verifier rejects
/// proofs that exceed plausible sizes through its own gas-style cost accounting.
#[cfg(feature = "runtime-benchmarks")]
type MaxProofNodes: Get<u32>;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if we shouldn't bound the proof nodes, in general (not just for runtime benchmarks)?

// `MaxMessageSize` (the upper bound on the inbound message payload).
let max_message_size = T::MaxMessageSize::get();
let weight_fee = T::WeightToFee::weight_to_fee(&T::WeightInfo::submit(
max_message_size,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, seems like it could result in a 25x increase in cost. We need to check if this won't result in unexpected problems.

Comment on lines +72 to +78
Weight::from_parts(225_000_000, 0)
.saturating_add(Weight::from_parts(0, 3775))
// Per proof-node cost: ~3 ms / node from receipt-trie traversal.
.saturating_add(Weight::from_parts(3_000_000, 0).saturating_mul(n.into()))
// Per receipt-byte cost: ~2 us / byte from RLP decode and log scan.
.saturating_add(Weight::from_parts(2_000, 0).saturating_mul(s.into()))
.saturating_add(T::DbWeight::get().reads(9))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have these and the Outbound V2 benchmarks been rerun? I noticed they look kinda of similar, checking that these have been run and not hardcoded.

// The receipt is stored as the value of the leaf (last) node of the receipt-trie
// proof, so the leaf's encoded length is a tight upper bound for the receipt size
// at dispatch time, before verification has run.
event.proof.receipt_proof.last().map(|leaf| leaf.len() as u32).unwrap_or(0),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
event.proof.receipt_proof.last().map(|leaf| leaf.len() as u32).unwrap_or(0),
event
.proof
.receipt_proof
.iter()
.fold(0u32, |acc, node| acc.saturating_add(node.len() as u32)),

This is more accurate than just using the leaf, by charging per-byte weight against the total size of all receipt-proof nodes. Because the caller can inflate trie nodes that verifier still has to hash without paying for that work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants